/********************************************************************************
 *	Copyright 2011 Gerry Weirich
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at 
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *	$Id$
 ********************************************************************************/

package ch.rgw.cybert
import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer

/**
 * A Node is one knowledge node. It consists of one or more Quants. It can compute its 'alikeness' to an other Node, and
 * it can merge an other Node into itself.
 * @author gerry
 * 
 */
class Node(text: String, creator: String) {
  private var label = text;
  private var rev: String = null;
  private var frequency = 0;

  protected var quants = Map[Int, Quant]();

  // Accessors
  def getLabel(): String = label;
  def getRevision = rev
  def setRevision(r: String) = rev = r
  def getFrequency = frequency;
  def setFrequency(f: Int) = frequency = f
  def getQuants = quants
  def getCreator = creator

  /**
   * Add a Quant to this Node. If a Quant with the same QuestionID exists already, 
   * only rebalance the existing Quant with the new Quant's value.
   * @param quant the Quant to add
   */
  def addQuant(quant: Quant) = {
    require(quant != null)
    val exists = quants.get(quant.getQuestionID);
    exists match {
      case None => quants += (quant.getQuestionID -> quant);
      case Some(existing) => existing.rebalance(quant.getValue, false);
    }
  }

  /**
   * Add a new Question. This is a convenience method for addQuant: Creates and configures a Quant and calls 
   * addQuant with this newly created Quant.
   * @param question The Question in text form
   * @param initialValue the initial/typical answer value
   * @param creator UUID of the creating system
   * @param phase phase of the questionaire, where this Question should be asked
   * @param ppv positive predictive value
   * @param npv negative predictive value
   */
  def addQuestion(question: String, initialValue: Int, creator: String, phase: Int, ppv: Float, npv: Float): Unit = {
    require(question != null && creator != null)
    require(question.length > 0 && creator.length > 0)
    val knolrsc = Cybert.nodersc;
    val idx = knolrsc.findOrAddQuestion(question);
    val quant = new Quant(idx, creator, phase);
    quant.setPpv(ppv)
    quant.setNpv(npv)
    quant.rebalance(initialValue, false);
    addQuant(quant);
  }
  /**
   * find out how similar this Node is to an other Node
   * @param other
   * @return the distance. The greater the number, the less alike are the Nodes
   */
  def computeTotalDistanceTo(other: Node): Double = {
    require(other != null)
    var ret = 0.0;
    var num = 0;
    quants.foreach { elem =>
      {
        var oquant = other.quants.get(elem._1);
        oquant match {
          case Some(x) => {
            val distance = elem._2.computeDistanceTo(x);
            if (distance._2) {
              ret += distance._1;
              num += 1
              if (Cybert.debug) {
                val sb = new StringBuilder
                sb.append("Computing distance of ").append(Cybert.nodersc.getQuestion(elem._1))
                  .append(" . Value-1: ").append(elem._2.getValue).append(" / Value-2: ").append(x.getValue)
                  .append(". => Distance was: ").append(distance._1);
                Cybert.log.info(sb.toString);
              }
            }
          }

          // If the Quant is only on one side: Fixed distance
          case None => {
            val value = elem._2.getValue
            if (value < -2 || value > 2) {
              ret += 0.4f;
              num += 1
            }
          }

        }
      }

      other.quants.foreach { elem =>
        {
          var q = quants.get(elem._1);
          q match {
            case None => {
              val value = elem._2.getValue
              if (value < -2 || value > 2) {
                ret += 0.4f;
                num += 1
              }
            }
            case Some(x) => // nothing; already processed
          }
        }
      }

    }
    // if this knol was rarely used, make it less probable
    if (frequency < 2) {
      ret += 0.5f
    }
    if (frequency < 4) {
      ret += 0.3f
    }
    ret / num;
  }

  /**
   * Merge this Node with an other. All Quants existing on both sides are rebalanced,
   * Quants existing only in "other" are added to this.
   * Quants existing only in "this" are left unchanged.
   * @param other the Node to merge. Remains unchanged.
   */
  def merge(other: Node) = {
    require(other != null)
    var intermediate = Map[Int, Quant]() ++ other.quants;
    quants.foreach { elem =>
      {
        var quant = elem._2;
        var other = intermediate.get(elem._1);
        if (other.isDefined) {
          quant.rebalance(other.get.getValue, false);
          intermediate -= elem._1;
        }
      }
    }
    intermediate.foreach { elem =>
      quants += elem;
    }

    frequency += 1;
  }

  /**
   * Create a human readable String to describe the contents of this knol.
   * @return
   */
  def dump: String = {
    val sb = new StringBuilder();
    val knolrsc = Cybert.nodersc;
    sb.append(getLabel).append(": ");
    listQuants.foreach(q => sb.append(knolrsc.getQuestion(q.getQuestionID).getOrElse("?")).append("->").append(q.getValue).append("\n"));
    return sb.toString;
  }
  /**
   * retrieve the quants as java List (for persistence purposes)
   */
  def listQuants: java.util.List[Quant] = {
    val ret = new ArrayBuffer[Quant];
    quants.foreach(q => ret.add(q._2));
    return ret;
  }

  /** equals and hashCode are implemented to make Maps and Lists work correctly. */
  override def equals(that: Any) = label == that.asInstanceOf[Node].label
  override def hashCode = if (label == "") "system-hypothesis".hashCode else label.hashCode
}
